/* Copyright (C) 2003, Charles Wang.
 * Author:  Charles Wang <charles@linux.net.cn>
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANT; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public LIcense for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, 59 Temple
 * Place - Suite 330, Boston, MA 02111-1307, USA.
 */
#include  <sys/types.h>
#include  <sys/stat.h>
#include  <sys/wait.h>
#include  <sys/mount.h>
#include  <dirent.h>
#include  <errno.h>
#include  <fcntl.h>
#include  <libgen.h>
#include  <stdio.h>
#include  <stdlib.h>
#include  <string.h>
#include  <unistd.h>
#include  "loop.h"
#include  "mount_guess_fstype.h"
#include  "magicbooter.h"
#include  "mblog.h"
#include  "utils.h"

static const char *mntdir_list[] = {
    mi_initrd_root,
    "/tmp/mnt1",
    "/tmp/mnt2",
    "/tmp/mnt3"
};
static const char *loopdev_list[] = {
    "/dev/loop7",
    "/dev/loop6",
    "/dev/loop5",
    "/dev/loop4"
};

static char * recur_valid_path(const char *path, int level,
			       umnt_info *ret);
char * probe_path(const char *path) {
    return  recur_valid_path(path, 1, NULL);
}
char * mount_path(const char *path, umnt_info *ret) {
    return  recur_valid_path(path, 1, ret);
}
void   umount_path(const umnt_info *ui) {
    int  curlevel, curmask;
    curlevel = ui->maxlevel;
    curmask = ui->blkmask;
    while (curlevel >= 1) {
	if (curmask & 1)
	    umount(mntdir_list[curlevel]);
	else
	    loumount(mntdir_list[curlevel]);
	--curlevel;
	curmask = curmask >> 1;
    }
}

extern unsigned  miinitrd_size;
char * recur_valid_path(const char *path, int level, umnt_info *ret) {
    char * curpath, * fname, * tmp, * mntpath;
    struct stat  sbuf;
    const char * fstype;

    curpath = strdupa(path);
    fname = basename(strdupa(path));

    for (;;) {
	if (!strcmp(curpath, "/"))  return  NULL;
	if (!strcmp(curpath, "."))  return  NULL;
	if (!stat(curpath, &sbuf))  break;
	curpath = dirname(curpath);
    }

    if (!S_ISBLK(sbuf.st_mode) && !S_ISREG(sbuf.st_mode)) {
	mblog("!S_ISBLK(sbuf.st_mode = 0%o) && !S_ISREG(sbuf.st_mode = 0%o)\n",
	      sbuf.st_mode, sbuf.st_mode);
	return  NULL;
    }

    tmp = strdupa(curpath);
    if (S_ISREG(sbuf.st_mode) &&
	/* sbuf.st_size == (off_t)miinitrd_size && // * 1024 * 1024 */
	!strcmp(basename(tmp), fname)) {
	/* Got it! */
	if (ret != NULL)  ret->maxlevel = level - 1;
        /* miinitrd_size = (unsigned)sbuf.st_size; */
	return  strdup(curpath);
    }

    fstype = do_guess_fstype(curpath);
    if (!fstype)  return  NULL;  /* Detect fstype failed. */
    if (!strcmp(fstype, "swap"))  return  NULL;
    if (S_ISBLK(sbuf.st_mode)) {
	if (mount(curpath, mntdir_list[level], fstype, MS_RDONLY, NULL)) {
	    mblog("mount('%s', '%s', '%s', MS_RDONLY, NULL) failed: %s.\n",
		  curpath, mntdir_list[level], fstype, strerror(errno));
	    return  NULL;
	}
    } else { /* S_ISREG(sbuf.st_mode) */
	if (lomount(curpath, mntdir_list[level], fstype)) {
	    mblog("lomount(%s, %s, %s) failed\n",
		  curpath, mntdir_list[level], fstype);
	    return  NULL;
	}
    }

    mntpath = alloca(strlen(mntdir_list[level]) + strlen(path + strlen(curpath)) + 1);
    sprintf(mntpath, "%s%s", mntdir_list[level], path + strlen(curpath));

    if (ret != NULL)
	ret->blkmask = (ret->blkmask << 1) + (S_ISBLK(sbuf.st_mode) ? 1 : 0);

    tmp = recur_valid_path(mntpath, level + 1, ret);

    if (ret == NULL || tmp == NULL) {
	if (S_ISBLK(sbuf.st_mode))
	    umount(mntdir_list[level]);
	else
	    loumount(mntdir_list[level]);
    }

    return  tmp;
}

static const char *search_device(const char *mntdir);
static int check_loopdev_unused(const char *device);
static int set_loop(const char *device, const char *file);
static int del_loop(const char *device);
int lomount(const char *file, const char *mntdir, const char *fstype) {
    /*
    char  cmdbuf[256];
    snprintf(cmdbuf, sizeof(cmdbuf),
	     "/bin/mount -r -o loop -t %s %s %s > /var/log/mount.out 2> /var/log/mount.err", fstype, file, mntdir);
    if (system(cmdbuf))  return -1;
    return  0;
    */
    const char *device = search_device(mntdir);

    mblog("lomount('%s', '%s', '%s'):device=[%s]\n", file, mntdir, fstype, device);
    if (check_loopdev_unused(device))  return -1;
    if (set_loop(device, file))  return -1;
    if (mount(device, mntdir, fstype, MS_RDONLY, NULL) == 0)  return 0;
    mblog("mount('%s', '%s', '%s', MS_RDONLY, NULL) failed: %s.\n",
	  device, mntdir, fstype, strerror(errno));
    del_loop(device);
    return -1;
}
int loumount(const char *mntdir) {
    /*
    char  cmdbuf[256];
    snprintf(cmdbuf, sizeof(cmdbuf), "/bin/umount %s", mntdir);
    if (system(cmdbuf))  return -1;
    return 0;
    */
    int  r = 0;
    const char *device = search_device(mntdir);

    mblog("loumount('%s'):device=[%s]\n", mntdir, device);
    if (umount(mntdir) != 0) {
	mblog("umount('%s') failed: %s.\n", mntdir, strerror(errno));
	r = 1;
    }
    if (del_loop(device)) {
	mblog("del_loop('%s') failed.\n", device);
	r = 1;
    }
    return r;
}

static const char *search_device(const char *mntdir) {
    int   i;
    for (i = 0; i < sizeof(mntdir_list) / sizeof(mntdir_list[0]); ++i)
	if (!strcmp(mntdir_list[i], mntdir))
	    return  loopdev_list[i];
    return  NULL;
}
static int check_loopdev_unused(const char *device) {
    int              fd;
    struct stat      statbuf;
    struct loop_info loopinfo;
    if (stat(device, &statbuf)) {
	mblog("stat %s failed: %s.\n", device, strerror(errno));
	return 1;
    }
    if (!S_ISBLK(statbuf.st_mode)) {
	mblog("%s is not a block device file.\n", device);
	return 1;
    }
    fd = open(device, O_RDONLY);
    if (fd < 0) {
	mblog("open %s failed: %s.\n", device, strerror(errno));
	return 1;
    }
    if (ioctl(fd, LOOP_GET_STATUS, &loopinfo) == 0) {
	mblog("%s is used already: %s.\n", device, strerror(errno));
	close(fd);
	return 1;
    }
    if (errno == ENXIO) {
	close(fd);
	return 0;
    }
    mblog("ioctl LOOP_GET_STATUS on %s failed: %s.\n",
	  device, strerror(errno));
    close(fd);
    return 1;
}
static int set_loop(const char *device, const char *file) {
    struct loop_info loopinfo;
    int              fd, ffd;

    if ((ffd = open(file, O_RDONLY)) < 0) {
	mblog("open('%s', O_RDONLY) failed: %s.\n", file, strerror(errno));
	return 1;
    }
    if ((fd = open(device, O_RDONLY)) < 0) {
	mblog("open('%s', O_RDONLY) failed: %s.\n", device, strerror(errno));
	close(ffd);
	return 1;
    }
    memset(&loopinfo, 0, sizeof(loopinfo));
    strncpy(loopinfo.lo_name, file, LO_NAME_SIZE - 1);
    loopinfo.lo_name[LO_NAME_SIZE - 1] = 0;
    loopinfo.lo_offset = 0;
    loopinfo.lo_encrypt_key_size = 0;
    if (ioctl(fd, LOOP_SET_FD, ffd) < 0) {
	mblog("ioctl('%s', LOOP_SET_FD, '%s') failed: %s.\n",
	      device, file, strerror(errno));
	close(fd);
	close(ffd);
	return 1;
    }
    if (ioctl(fd, LOOP_SET_STATUS, &loopinfo) < 0) {
	(void)ioctl(fd, LOOP_CLR_FD, 0);
	mblog("ioctl('%s', LOOP_SET_STATUS, &loopinfo) failed: %s.\n",
	      device, strerror(errno));
	close(fd);
	close(ffd);
	return 1;
    }
    close(fd);
    close(ffd);
    return 0;
}
static int del_loop(const char *device) {
    int fd;
    if ((fd = open(device, O_RDONLY)) < 0) {
	mblog("open('%s', O_RDONLY) failed: %s.\n",
	      device, strerror(errno));
	return 1;
    }
    if (ioctl(fd, LOOP_CLR_FD, 0) < 0) {
	mblog("ioctl('%s', LOOP_CLR_FD, 0) failed: %s.\n",
	      device, strerror(errno));
	close(fd);
	return 1;
    }
    close(fd);
    return 0;
}

int exec_and_wait(char *argv[]) {
    pid_t  pid;
    int    r, status;

    if ((pid = fork()) < 0) {
	mblog("Fork to exec %s failed: %s\n", argv[0], strerror(errno));
	return -1;
    } else if (pid == 0) {
	execv(argv[0], argv);
	exit(-1);
    }

    r = waitpid(pid, &status, 0);
    if (r < 0) {
        mblog("waitpid error: %s\n", strerror(errno));
        return -1;
    }
    else
        return 0;
}

int do_modprobe(char *module) {
    char *argv[] = {"/sbin/modprobe", module, NULL};

    if (check_kmodule_loaded(module)) {
	mblog("The module '%s' has been loaded already.\n", module);
	return 0;
    }

    if (exec_and_wait(argv)) {
	mblog("Modprobe failed: %s\n", strerror(errno));
	return -1;
    }

    if (check_kmodule_loaded(module)) {
	mblog("Modprobe %s success.\n");
	return  0;
    }
    else
        return -1;
}

/* argv[0] = "/sbin/modprobe"
   argv[1] = "module"
   argv[2] = "option1"
   argv[3] = "option2"
   ...
   argv[end] = NULL
*/
int  do_modprobe2(char* argv[]) {
    char *module = argv[1];

    if (check_kmodule_loaded(module)) {
	mblog("The module '%s' has been loaded already.\n", module);
	return 0;
    }

    if (exec_and_wait(argv)) {
	mblog("Modprobe failed: %s\n", strerror(errno));
	return -1;
    }

    if (check_kmodule_loaded(module)) {
	mblog("Modprobe %s success.\n");
	return  0;
    }
    else
        return -1;
}

#ifdef  USELESS
/* load_all_filesystem_modules is useless because the filesystem modules
   can be loaded automatically now. */
void load_all_filesystem_modules(void) {
    char   dirbuf[128], fnamebuf[128], cmdbuf[128];
    DIR  * d;  struct dirent *de;
    struct stat sbuf;

    snprintf(dirbuf, sizeof(dirbuf), "/lib/modules/%s/kernel/fs", kernelver);
    d = opendir(dirbuf);
    while ((de = readdir(d)) != NULL) {
	if (!strcmp(".",  de->d_name))  continue;
	if (!strcmp("..", de->d_name))  continue;
	snprintf(fnamebuf, sizeof(fnamebuf),
		 "%s/%s/%s.o", dirbuf, de->d_name, de->d_name);
	if (lstat(fnamebuf, &sbuf))  continue;
	if (!S_ISREG(sbuf.st_mode))  continue;
	if (check_filesystem(de->d_name))  continue;

	if (fork() == 0) {
	    int wfd;
	    wfd = open("/var/log/modprobe.log", O_WRONLY | O_CREAT | O_APPEND);
	    dup2(wfd, 1);  dup2(wfd, 2);
	    execl("/sbin/modprobe", "/sbin/modprobe", de->d_name, NULL);
	    exit(-1);
	}
	mblog("Load module %s\n", de->d_name);
    }
    closedir(d);
}
#endif

int  check_kmodule_loaded(const char *module) {
    FILE *mfp;
    char  linebuf[512];  int  slmodule;

    if ((mfp = fopen("/proc/modules", "r")) == NULL)
	return 0;
    slmodule = strlen(module);
    while (fgets(linebuf, sizeof(linebuf), mfp))
	if (strcspn(linebuf, " \t") == slmodule &&
	    !memcmp(linebuf, module, slmodule)) {
	    fclose(mfp);
	    return 1;
	}
    fclose(mfp);
    return 0;
}

int  check_filesystem(const char *fstype) {
    FILE *ffp;
    char  linebuf[512], *cur;  int slfstype;

    if ((ffp = fopen("/proc/filesystems", "r")) == NULL)
	return 0;
    slfstype = strlen(fstype);
    while (fgets(linebuf, sizeof(linebuf), ffp)) {
	cur = linebuf + strcspn(linebuf, " \t");
	cur += strspn(linebuf, " \t");
	if (strlen(cur) == slfstype + 1 && cur[slfstype] == '\n' &&
	    !memcmp(cur, fstype, slfstype)) {
	    fclose(ffp);
	    return 1;
	}
    }
    fclose(ffp);
    return 0;
}

#ifdef USELESS
/* guess_load_fstype is useless because the filesystem modules
   can be loaded automatically now. */
const char *guess_load_fstype(const char *dev_file) {
    char  *fstype;
    char   cmdbuf[128];

    fstype = do_guess_fstype(dev_file);
    if (!fstype)  return NULL;
    if (check_filesystem(fstype))  return fstype;
    snprintf(cmdbuf, sizeof(cmdbuf), "/sbin/modprobe %s 2> /dev/null", fstype);
    system(cmdbuf);
    if (check_filesystem(fstype))  return fstype;
    return  NULL;
}
#endif

/* Date struct and operations about search result. */
char  re_space[16384], * re_space_firstfree;
const char * re_list[256];
const unsigned  max_re_list = sizeof(re_list) / sizeof(re_list[0]);
unsigned     num_re_list;

void  relist_clear(void) {
    re_space_firstfree = re_space;
    num_re_list = 0;
}
void  relist_append(const char *result) {
    int  addlen;
    addlen = strlen(result);
    if (re_space_firstfree - re_space + addlen + 1 > sizeof(re_space))
	/* Not enough space, omit this result. */
	return;
    if (num_re_list >= max_re_list)
	/* Not enough result list slot, omit this result. */
	return;
    strcpy(re_space_firstfree, result);
    re_list[num_re_list] = re_space_firstfree;
    re_space_firstfree += addlen + 1;
    ++num_re_list;
}
const char * relist_first(char **iter) {
    if  (re_space >= re_space_firstfree)  return  NULL;
    *iter = re_space + strlen(re_space) + 1;
    return  re_space;
}
const char * relist_next(char **iter) {
    char  * result;
    if (*iter >= re_space_firstfree)  return  NULL;
    result = *iter;
    *iter = result + strlen(result) + 1;
    return  result;
}

static char  cur_drvtype[16] = "";
static int   sz_drvlist;
static char *drvlist[384];
static char  drvspace[6144];

static int bsearch_strcmp(const void *v1, const void *v2) {
    return  strcmp(v1, *((const char **)v2));
}
static void load_drvlist(const char *drvtype);
int  check_drv(const char *module, const char *drvtype) {
    if (strcmp(drvtype, cur_drvtype))  load_drvlist(drvtype);
    return  bsearch(module, drvlist, sz_drvlist, sizeof(char *), bsearch_strcmp) != NULL;
}

static int qsort_strcmp(const void *v1, const void *v2) {
    return  strcmp(*((const char **)v1), *((const char **)v2));
}
static char * recur_load_drvlist(char *dirnamebuf, char *space, int szspace);
static void load_drvlist(const char *drvtype) {
    char  dirnamebuf[256];
    sz_drvlist = 0;
    snprintf(dirnamebuf, sizeof(dirnamebuf),
	     "/lib/modules/%s/kernel/drivers/%s/", kernelver, drvtype);
    recur_load_drvlist(dirnamebuf, drvspace, sizeof(drvspace));
    qsort(drvlist, sz_drvlist, sizeof(drvlist[0]), qsort_strcmp);
    strcpy(cur_drvtype, drvtype);
}

static char * recur_load_drvlist(char *dirnamebuf, char *space, int szspace) {
    DIR  * d;  struct dirent * de;
    struct stat sbuf;
    char * pos;
    int    l;  char * newspace;

    d = opendir(dirnamebuf);
    if (!d)  return  space;

    pos = dirnamebuf + strlen(dirnamebuf);
    while (szspace > 0 &&
	   sz_drvlist < sizeof(drvlist) / sizeof(drvlist[0]) &&
	   (de = readdir(d)) != NULL) {
	strcpy(pos, de->d_name);
	if (lstat(dirnamebuf, &sbuf))  continue;
	if (S_ISREG(sbuf.st_mode)) {
	    l = strlen(de->d_name);
	    if (l > 2 && szspace > l - 2 + 1 &&
		de->d_name[l - 2] == '.' && de->d_name[l - 1] == 'o') {
		de->d_name[l - 2] = 0;
		drvlist[sz_drvlist++] = space;
		strcpy(space, de->d_name);
		space += l - 2 + 1;
	    }
	    else if (l > 3 && szspace > l - 3 + 1 &&
                     de->d_name[l - 3] == '.' && de->d_name[l - 2] == 'k' && de->d_name[l - 1] == 'o') {
		de->d_name[l - 3] = 0;
		drvlist[sz_drvlist++] = space;
		strcpy(space, de->d_name);
		space += l - 3 + 1;
	    }
	} else if (S_ISDIR(sbuf.st_mode)) {
	    if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
		continue;
	    strcat(pos, "/");
	    newspace = recur_load_drvlist(dirnamebuf, space, szspace);
	    szspace -= newspace - space;
	    space = newspace;
	}
    }

    closedir(d);
    return  space;
}

/* Walk down the tree of _dir, if walk_func return non-zero, the process will
   stop. Return 1 if process is stopped by walk_func, -1 if error occurs,
   otherwise return 0. */
int walk_tree(const char *_dir,
              int (*walk_func)(const char *_dirname,
                               const char *_basename, 
                               struct stat *_mode))
{
    DIR  * d;
    struct dirent * de;
    struct stat sbuf;
    char dirnamebuf[256];
    char * pos;
    int len;
    int rc = 0;

    xstrncpy(dirnamebuf, _dir, sizeof(dirnamebuf), 0);
    len = strlen(dirnamebuf);
    if (dirnamebuf[len-1] != '/'
        && len < sizeof(dirnamebuf)-1) {
        dirnamebuf[len++] = '/';
        dirnamebuf[len] = '\0';
    }
    if (dirnamebuf[len-1] != '/') {
        rc = -1;
        goto err1;
    }

    pos = dirnamebuf + len;
    d = opendir(dirnamebuf);
    if (!d) {
        rc = -1;
        goto err1;
    }

    while (NULL != (de = readdir(d))) {
	xstrncpy(pos, de->d_name,
                 sizeof(dirnamebuf) - len, 0);
        
	if (lstat(dirnamebuf, &sbuf)) {
            continue;
        }
        if (walk_func(_dir, de->d_name, &sbuf)) {
            rc = 1;
            break;
        }
        /* recur if is directory */
	if (S_ISDIR(sbuf.st_mode)) {
	    if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
		continue;
            if (walk_tree(dirnamebuf, walk_func) == 1) {
                rc = 1;
                break;
            }
	}
    }
    closedir(d);
  err1:
    return rc;
}
